Skip to content

Add multi-provider search/fetch failover and block empty newsletters from being sent - #8

Merged
kevinhermawan merged 1 commit into
mainfrom
feat/multi-provider-search-failover
Jun 19, 2026
Merged

Add multi-provider search/fetch failover and block empty newsletters from being sent#8
kevinhermawan merged 1 commit into
mainfrom
feat/multi-provider-search-failover

Conversation

@kevinhermawan

Copy link
Copy Markdown
Collaborator

Summary

On 2026-06-19, the Serper credit account hit its hard limit mid-campaign, causing 527 consecutive
search failures. Seven newsletters (EXCL, FORE, MAPI, ERAA, MAPA, SMMA, DSSA) were generated with
no editorial sections, yet were finalized as complete and queued for delivery. This PR closes both
the resilience gap and the safety gap.

Related issues

Closes #7

Changes

  • src/agents/tools/providers/ (new package): round-robin + failover dispatcher (dispatch.py)
    and vendor adapters for Serper, Exa, Tavily, Firecrawl, and Diffbot. Each adapter exposes the
    same search()/fetch() interface so the dispatcher is fully hidden from the LLM.
  • src/agents/tools/web_search.py (new, replaces search.py): routes web_search() through
    SEARCH_PROVIDERS = [serper, exa, tavily] with round-robin + failover. Renamed search to
    web_search for symmetry with web_fetch.
  • src/agents/tools/web_fetch.py (updated): routes through FETCH_PROVIDERS = [serper, exa, tavily, firecrawl, diffbot]. Never raises to the writer -- falls back to a "summarize from
    snippet" message if all providers fail.
  • src/agents/campaign.py (updated): deliver() checks has_sections() before rendering or
    sending. An empty newsletter is finalized as status="failed" and its recipients are skipped.
  • src/emails/templates/newsletter.py (updated): adds has_sections(), reusing _parse() so
    the gate matches the renderer exactly.
  • Skills updated (section-research/SKILL.md, subject-profile/SKILL.md): search references
    changed to web_search.
  • .env.example and CLAUDE.md updated with the new provider keys.
  • Tests: test_tools.py fully rewritten for the new module layout; new dispatch engine tests,
    adapter tests, and multi-provider failover tests added. test_campaign.py and
    test_email_template.py cover the empty-newsletter guard.

How to test

  1. ruff check . && ruff format --check . -- both must pass.
  2. cd email-playground && npm run build:templates && cd .. && pytest -- 124 tests, all green.
  3. Set at least two search provider keys (SERPER_API_KEY, EXA_API_KEY) and run
    python src/app.py test --email=you@example.com (dry-run). Check agent_activity to confirm
    web_search calls are spread across providers.
  4. To verify the empty-newsletter guard: point SERPER_API_KEY and EXA_API_KEY at invalid values,
    run the same command, and confirm the ticker is logged skipped ... no sections, its newsletter
    row is status="failed", and no email is attempted.

Serper credit exhaustion on 2026-06-19 produced seven sectionless newsletters
that were still marked complete and queued for delivery. This commit fixes
both the resilience and the safety gaps.

Resilience: replace the single-provider Serper integration with a
deterministic round-robin + failover dispatcher behind a new
tools/providers/ package. Serper, Exa, and Tavily each provide search +
fetch; Firecrawl and Diffbot add fetch-only. The dispatcher is hidden from
the LLM entirely -- the tool signatures (web_search / web_fetch) are
unchanged. A provider joins the rotation as soon as its API key env var is
set, and drops out automatically when the key is absent or the call errors.

Safety: newsletters with no editorial sections (no cited [Read: ...] items)
are finalized with status="failed" and never emailed. has_sections() reuses
_parse() from the email renderer so the gate matches exactly what would have
been sent.

Also renames the search tool to web_search for symmetry with web_fetch and
updates the two SKILL.md references accordingly.
@kevinhermawan
kevinhermawan merged commit a333f52 into main Jun 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add multi-provider search/fetch failover and block empty newsletters from being sent

1 participant